Projet P5 - Segmentez des clients d'un site e-commerce¶
OPENCLASSROOMS - Parcours Data Scientist - Adeline Le Ray - 03/2024
Introduction¶
Segmentation des clients : L'objectif est de comprendre les différents types d’utilisateurs grâce à leur comportement et à leurs données personnelles. La segmentation proposée doit être exploitable et facile d’utilisation par l'équipe Marketing. Elle doit au minimum pouvoir différencier les bons et moins bons clients en termes de commandes et de satisfaction.
Les méthodes utilisées sont :
- RFM marketing
- Méthodes non supervisées :
- K-means
- Classification Ascendante Hiérarchique
- DBScan
Essais :
- Variables RFM : déterminer la méthode la plus efficace
- Variables RFM + satisfaction
- Plus de variables : ACP + clustering
Sommaire¶
Notebook 1 - Requêtes SQL
Notebook 2 - Analyse exploratoire
Notebook 3 - Essais clustering
Partie 2 - RFM avec méthodes d'apprentissage non supervisé
Partie 3 - Clustering avec les variables RFM et satisfaction clients
Partie 4 - Analyse en Composantes Principales puis Clustering
Notebook 4 - Simulation maintenance
Importation des librairies et des données¶
import numpy as np
import pandas as pd
import math
# graphiques
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import os
from joblib import Parallel, delayed
from IPython.display import Markdown # affichage Markdown des Outputs
# Principal Composante Analysis
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Classification Ascendante Hierarchique
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
# K-Means / DBScan
from sklearn.cluster import KMeans, DBSCAN
from sklearn.neighbors import NearestNeighbors
from sklearn.metrics import silhouette_score
from yellowbrick.cluster import SilhouetteVisualizer
from yellowbrick.cluster import KElbowVisualizer
# Version python
!python --version
# Version des librairies utilisées
print('\n'.join(f'{m.__name__} - {m.__version__}'
for m in globals().values()
if getattr(m, '__version__', None)))
Python 3.11.4 numpy - 1.26.4 pandas - 2.1.1 seaborn - 0.13.0
# Paramètres par défauts des graphiques
sns.set_style('whitegrid') # darkgrid, white grid, dark, white and ticks
plt.rc('axes', titlesize=15) # fontsize of the axes title
plt.rc('axes', labelsize=14) # fontsize of the x and y labels
plt.rc('xtick', labelsize=13) # fontsize of the tick labels
plt.rc('ytick', labelsize=13) # fontsize of the tick labels
plt.rc('legend', fontsize=13) # legend fontsize
plt.rc('font', size=13) # controls default text sizes
width = 7
height = 5
plt.figure(figsize=(width, height))
meanprops = {'marker':'o', 'markeredgecolor':'black','markerfacecolor':'firebrick'}
<Figure size 700x500 with 0 Axes>
# Options d'affichage : toutes les colonnes
pd.set_option('display.max_columns', None)
os.environ["OMP_NUM_THREADS"] = '1'
# Définition de la palette de couleur
my_palette = px.colors.qualitative.Plotly
df_cleaned = pd.read_pickle('df_cleaned.pkl')
df_cleaned.set_index(df_cleaned.columns[0], inplace=True)
Définition des fonctions¶
# Fonction pour générer un radar plot par ligne du dataframe
def make_spider(data, group, row, title, color):
"""!
@brief Génère un radar plot pour une ligne d'un dataframe.
Cette fonction permet de générer un radar plot pour une ligne d'un dataframe (exemple : centroïdes de clusters).
@param data: Dataframe contenant les données (pandas DataFrame).
@param group: Variable contenant l'intitulé du groupe (type string)
@param row : ligne du dataframe (type integer)
@param title : titre du radar plot (type string)
@param color : couleur du radar plot (type string)
"""
# Nombre de variables
categories = list(data)[1:]
N = len(categories)
# Angle de chaque axe dans le plot (nous divisons le plot / nombre de variables)
angles = [n / float(N) * 2 * math.pi for n in range(N)]
angles += angles[:1]
# Définir le nombre de sous-tracés
if len(data.index) % 2 != 0:
line = round((len(data.index) + 1) / 2)
else:
line = round(len(data.index) / 2)
# Initialiser le radar plot
ax = plt.subplot(line, 2, row + 1, polar=True)
# Si vous voulez que le premier axe soit en haut :
ax.set_theta_offset(math.pi / 2)
ax.set_theta_direction(-1)
# Dessiner un axe par variable + ajouter les libellés
plt.xticks(angles[:-1], categories, color='grey', size=8)
# Dessiner les libellés y
ax.set_rlabel_position(0)
# Echelle du tracé
ylim_min = data.select_dtypes(include='number').min().min()
ylim_max = data.select_dtypes(include='number').max().max()
plt.ylim(ylim_min,ylim_max)
# Valeurs de la ligne spécifiée
values = data.loc[row].drop(group).values.flatten().tolist()
values += values[:1]
ax.plot(angles, values, color=color, linewidth=2, linestyle='solid')
ax.fill(angles, values, color=color, alpha=0.4)
# Ajouter un titre
plt.title(title, size=11, color=color, y=1.1)
plt.tight_layout()
def correlation_graph(pca,
x_y,
features) :
"""!
@brief Affiche le graphe des correlations
Positional arguments :
-----------------------------------
@param pca : sklearn.decomposition.PCA : notre objet PCA qui a été fit
@param x_y : list ou tuple : le couple x,y des plans à afficher, exemple [0,1] pour F1, F2
@param features : list ou tuple : la liste des features (ie des dimensions) à représenter
"""
# Extrait x et y
x,y=x_y
# Taille de l'image (en inches)
fig, ax = plt.subplots(figsize=(15, 11))
# Pour chaque composante :
for i in range(0, pca.components_.shape[1]):
# Les flèches
ax.arrow(0,0,
pca.components_[x, i],
pca.components_[y, i],
head_width=0.07,
head_length=0.07,
width=0.02, )
# Les labels
plt.text(pca.components_[x, i] + 0.05,
pca.components_[y, i] + 0.05,
features[i])
# Affichage des lignes horizontales et verticales
plt.plot([-1, 1], [0, 0], color='grey', ls='--')
plt.plot([0, 0], [-1, 1], color='grey', ls='--')
# Nom des axes, avec le pourcentage d'inertie expliqué
plt.xlabel('PC{} ({}%)'.format(x+1, round(100*pca.explained_variance_ratio_[x],1)))
plt.ylabel('PC{} ({}%)'.format(y+1, round(100*pca.explained_variance_ratio_[y],1)))
plt.title("Cercle des corrélations (PC{} et PC{})".format(x+1, y+1))
# Le cercle
an = np.linspace(0, 2 * np.pi, 100)
plt.plot(np.cos(an), np.sin(an)) # Add a unit circle for scale
# Axes et display
plt.axis('equal')
plt.show(block=False)
def display_factorial_planes( X_projected,
x_y,
pca=None,
labels = None,
clusters=None,
alpha=1,
figsize=[10,8],
marker="." ):
"""!
@brief Affiche la projection des individus
Positional arguments :
-------------------------------------
@param X_projected : np.array, pd.DataFrame, list of list : la matrice des points projetés
@param x_y : list ou tuple : le couple x,y des plans à afficher, exemple [0,1] pour PC1, PC2
Optional arguments :
-------------------------------------
@param pca : sklearn.decomposition.PCA : un objet PCA qui a été fit, cela nous permettra d'afficher la variance de chaque composante, default = None
@param labels : list ou tuple : les labels des individus à projeter, default = None
@param clusters : list ou tuple : la liste des clusters auquel appartient chaque individu, default = None
@param alpha : float in [0,1] : paramètre de transparence, 0=100% transparent, 1=0% transparent, default = 1
@param figsize : list ou tuple : couple width, height qui définit la taille de la figure en inches, default = [10,8]
@param marker : str : le type de marker utilisé pour représenter les individus, points croix etc etc, default = "."
"""
# Transforme X_projected en np.array
X_ = np.array(X_projected)
# Forme de la figure
if not figsize:
figsize = (7,6)
# Labels
if labels is None :
labels = []
try :
len(labels)
except Exception as e :
raise e
# Vérification de la variable axis
if not len(x_y) ==2 :
raise AttributeError("2 axes sont demandées")
if max(x_y )>= X_.shape[1] :
raise AttributeError("la variable axis n'est pas bonne")
# Définition de x et y
x, y = x_y
# Initialisation de la figure
fig, ax = plt.subplots(1, 1, figsize=figsize)
# Vérification de la présence de clustrers ou non
c = None if clusters is None else clusters
# Les points
# plt.scatter( X_[:, x], X_[:, y], alpha=alpha,
# c=c, cmap="Set1", marker=marker)
sns.scatterplot(data=None, x=X_[:, x], y=X_[:, y], hue=c)
# Si la variable pca a été fournie, on peut calculer le % de variance de chaque axe
if pca :
v1 = str(round(100*pca.explained_variance_ratio_[x])) + " %"
v2 = str(round(100*pca.explained_variance_ratio_[y])) + " %"
else :
v1=v2= ''
# Nom des axes, avec le pourcentage d'inertie expliqué
ax.set_xlabel(f'PC{x+1} {v1}')
ax.set_ylabel(f'PC{y+1} {v2}')
# Valeur x max et y max
x_max = np.abs(X_[:, x]).max() *1.1
y_max = np.abs(X_[:, y]).max() *1.1
# Bornes pour x et y
ax.set_xlim(left=-x_max, right=x_max)
ax.set_ylim(bottom= -y_max, top=y_max)
# Affichage des lignes horizontales et verticales
plt.plot([-x_max, x_max], [0, 0], color='grey', alpha=0.8)
plt.plot([0,0], [-y_max, y_max], color='grey', alpha=0.8)
# Affichage des labels des points
if len(labels) :
for i,(_x,_y) in enumerate(X_[:,[x,y]]):
plt.text(_x, _y+0.05, labels[i], fontsize='14', ha='center',va='center')
# Titre et display
plt.title(f"Projection des individus (sur PC{x+1} et PC{y+1})")
plt.show()
Partie 1 - RFM Marketing¶
La segmentation RFM prend en compte les variables suivantes pour établir des segments de clients homogènes :
- la Récence (date de la dernière commande),
- la Fréquence des commandes
- le Montant (de la dernière commande ou sur une période donnée)
# Dataframe RFM
rfm_df = df_cleaned[['recency', 'frequency', 'monetary']]
# Centrage et réduction des variables
X_rfm = rfm_df.values # Matrice des données
names = rfm_df.index # Noms des individus
features_rfm = ['recency','frequency','monetary'] # Noms des variables
scaler = StandardScaler() # Instanciation du scaler
X_rfm_scaled = scaler.fit_transform(X_rfm) # Données scalées
Définition du RFM score¶
Pour la fréquence des achats, comme seuls 3% des clients ont effectué plus de 1 achat, la discétisation du score frequency ne se fera pas sur la base des quartiles. Les classes seront définies manuellement.
# Nombre de clients par fréquence d'achats
rfm_df.groupby('frequency', as_index=False)['recency'].count()
| frequency | recency | |
|---|---|---|
| 0 | 1 | 90254 |
| 1 | 2 | 2562 |
| 2 | 3 | 180 |
| 3 | 4 | 27 |
| 4 | 5 | 9 |
| 5 | 6 | 5 |
| 6 | 7 | 3 |
| 7 | 9 | 1 |
| 8 | 15 | 1 |
# RFM score
rfm_df = rfm_df.copy()
rfm_df['R'] = pd.qcut(rfm_df['recency'], 5, labels=[5, 4, 3, 2, 1])
rfm_df['F'] = pd.cut(rfm_df['frequency'], [1, 2, 3, 4, 6, 16], labels=[1, 2, 3, 4, 5], right=False)
rfm_df['M'] = pd.qcut(rfm_df['monetary'], 5, labels=[1, 2, 3, 4, 5])
rfm_df['RFM_Score'] = rfm_df['R'].astype(str) + rfm_df['F'].astype(str) + rfm_df['M'].astype(str)
# Tailles des différentes classes
rfm_df.loc[:,['R','F','M']] = rfm_df[['R','F','M']].astype(int)
size_R = rfm_df.groupby('R', as_index=False,observed=True).size().rename(columns={'size':'size_R'}).sort_values('R')
size_F = rfm_df.groupby('F', as_index=False,observed=True).size().rename(columns={'size':'size_F'}).sort_values('F')
size_M = rfm_df.groupby('M', as_index=False,observed=True).size().rename(columns={'size':'size_M'}).sort_values('M')
size_rfm = pd.DataFrame({'class' : size_R['R'],
'size_R':size_R['size_R'],
'size_F':size_F['size_F'],
'size_M':size_M['size_M']}
)
display(size_rfm)
| class | size_R | size_F | size_M | |
|---|---|---|---|---|
| 0 | 5 | 18630 | 90254 | 18615 |
| 1 | 4 | 18643 | 2562 | 18602 |
| 2 | 3 | 18642 | 180 | 18608 |
| 3 | 2 | 18617 | 36 | 18612 |
| 4 | 1 | 18510 | 10 | 18605 |
Segmentation RFM Marketing¶
# Dictionnaire pour la correspondance des segments basés sur recency et frequency
seg_map = {
r'[1-2][1-2]': 'Hibernating',
r'[1-2][3-5]': 'At Risk',
r'[3][1-3]': 'Need Attention',
r'[4-5][1-3]': 'Potential Loyalists',
r'[3-4][4-5]': 'Loyal Customers',
r'5[4-5]': 'Champions'
}
# Attribution des segments
rfm_df.loc[:, 'Segment'] = rfm_df['R'].astype(str) + rfm_df['F'].astype(str)
rfm_df.loc[:,'Segment'] = rfm_df['Segment'].replace(seg_map, regex=True)
rfm_df.head()
| recency | frequency | monetary | R | F | M | RFM_Score | Segment | |
|---|---|---|---|---|---|---|---|---|
| customer_unique_id | ||||||||
| 0000366f3b9a7992bf8c76cfdf3221e2 | 160.0 | 1 | 141.90 | 4 | 1 | 4 | 414 | Potential Loyalists |
| 0000b849f77a49e4a4ce2b2a4ca5be3f | 163.0 | 1 | 27.19 | 4 | 1 | 1 | 411 | Potential Loyalists |
| 0000f46a3911fa3c0805444483337064 | 586.0 | 1 | 86.22 | 1 | 1 | 2 | 112 | Hibernating |
| 0000f6ccb0745a6a4b88665a16c9f078 | 370.0 | 1 | 43.62 | 2 | 1 | 1 | 211 | Hibernating |
| 0004aac84e0df4da2b147fca70cf8255 | 337.0 | 1 | 196.89 | 2 | 1 | 4 | 214 | Hibernating |
# Caractéristiques moyennes de chaque segment
centroids_rfm = rfm_df.groupby('Segment')[['recency','frequency','monetary']].mean().sort_values('monetary')
Analyse des segments¶
# Nombre de clients par segment RFM
plt.figure(figsize=(10,5))
segment_counts = rfm_df['Segment'].value_counts()
sns.barplot(x=segment_counts.index, y=segment_counts.values, hue=segment_counts.index, palette = my_palette[:6])
# Ajouter les valeurs au-dessus de chaque barre
for i, value in enumerate(segment_counts.values):
plt.text(i, value, str(value), ha='center', va='bottom')
plt.title('Nombre de clients par segment')
plt.xlabel('Segment')
plt.ylabel('Nombre de clients')
plt.xticks(rotation=45)
plt.show()
# Nombre de clients par segment RFM
segment_counts = rfm_df['Segment'].value_counts().reset_index()
segment_counts.columns = ['Segment', 'Count']
fig = px.treemap(segment_counts, path=['Segment'], values='Count')
fig.update_layout(
title="Segments RFM - Nombre de clients",
margin=dict(t=50, l=0, r=0, b=0)
)
fig.show()
# Valeurs monétaires des clients par segment RFM
segment_monetary = rfm_df.groupby('Segment', as_index=False)['monetary'].sum()
fig = px.treemap(segment_monetary, path=['Segment'], values='monetary')
fig.update_layout(
title="Segments RFM - Monetary",
margin=dict(t=50, l=0, r=0, b=0) # Marge pour le titre
)
fig.show()
# Projection sur les 3 variables RFM - graphique 3D
fig = px.scatter_3d(
rfm_df, x='recency', y='frequency', z='monetary', color='Segment',
title='Segmentation RFM Marketing'
)
fig.show()
# Pairplot des segments RFM
data = rfm_df[['recency', 'monetary','frequency', 'Segment']]
sns.pairplot(data,
hue='Segment',
palette=my_palette[:6],
dropna = True,
corner = True).fig.suptitle('Pairplot des Segments RFM', y=1.05)
plt.show()
# Standardisation (centrage-reduction) des valeurs des centroïdes
centroids_rfm_scaled = pd.DataFrame(scaler.fit_transform(centroids_rfm.values),
index = centroids_rfm.index,
columns = features_rfm )
# Reset index pour visualisation radar charts
centroids_rfm_scaled_df = centroids_rfm_scaled.reset_index()
centroids_rfm_scaled_df = centroids_rfm_scaled_df.rename(columns={'index':'Segment'})
# Représentation des centroïdes sur des radar charts
plt.figure(figsize=(10, 10))
# Boucle pour afficher les radars charts
for row in range(0, len(centroids_rfm_scaled_df.index)):
make_spider(data=centroids_rfm_scaled_df,
group='Segment',
row=row,
title='Cluster '+str(centroids_rfm_scaled_df['Segment'][row]),
color=my_palette[row])
# Transformation du dataframe pour avoir les valeurs en colonne -> nécessaire pour la représentation en radar chart
centroids_rfm_scaled_melt = centroids_rfm_scaled.reset_index().melt(id_vars = 'Segment',
value_vars = centroids_rfm_scaled.columns)
# Créer des traces pour chaque segment
traces = []
for segment in centroids_rfm_scaled_melt['Segment'].unique():
segment_data = centroids_rfm_scaled_melt[centroids_rfm_scaled_melt['Segment'] == segment]
# Ajouter le premier point à la fin pour fermer la ligne
r_values = segment_data['value'].tolist() + [segment_data['value'].iloc[0]]
theta_values = segment_data['variable'].tolist() + [segment_data['variable'].iloc[0]]
trace = go.Scatterpolar(
r=r_values,
theta=theta_values,
mode='lines',
name=segment,
fill='toself' # Remplir l'aire sous la courbe
)
traces.append(trace)
# Créer la mise en page
layout = go.Layout(
title="Comparaison des centroïdes des segments RFM"
)
# Créer la figure
fig = go.Figure(data=traces, layout=layout)
# Afficher la figure
fig.show()
Partie 2 - RFM avec méthodes d'apprentissage non supervisé¶
K-Means¶
Nombre optimum de clusters k¶
Différentes méthodes peuvent être utilisées pour déterminer le nombre optimal de clusters k :
- Méthode du coude sur le distorsion score : distorsion score = moyenne de la somme des carrés des écarts de distances entre les individus d'un cluster et son centroïde
$$distortion = \frac{1}{n}\Sigma(distance(point, centroïde)^2)$$
- Méthode du coude sur l'inertie : inertie = somme des carrés des écarts de distances entre les individus d'un cluster et son centroïde
$$inertie = \Sigma(distance(point, centroïde)^2)$$
- Silhouette score / plot : Le coefficient de silhouette est la moyenne des différences entre la distance moyenne d'un point avec les points du même groupe que lui (cohésion) et la distance moyenne du point avec les points des autres groupes voisins (séparation). Plus le coefficient de silhouette est élevé, meilleure est la classification
Nombre optimal de clusters : L'ensemble des méthodes donnent comme optimum 4 clusters.
# définition du random_state et k range
k_min = 2
k_max = 10
random_state = 42
# Distortion score with KElbow visualizer
# Instanciation du modèle de clustering et du visualizer
km = KMeans(n_init=10, init='k-means++', random_state=random_state)
kvisualizer = KElbowVisualizer(km, k=(k_min, k_max))
# Entraînement du visualizer et affichage du graphique
kvisualizer.fit(X_rfm_scaled)
kvisualizer.show()
plt.show()
# Récupération de la valeur du coude
k_rfm = kvisualizer.elbow_value_
# Silhouette score
silhouette_scores = []
for k in range(k_min, k_max):
kmeans = KMeans(n_clusters=k, n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_rfm_scaled)
labels = kmeans.labels_
silhouette_scores.append(silhouette_score(X_rfm_scaled, labels))
# Représentation graphique du Silhouette scor
plt.plot(range(k_min, k_max), silhouette_scores)
plt.title('Coefficient de silhouette pour chaque nombre de clusters k')
plt.xlabel('Nombre de clusters')
plt.ylabel('Coefficient de silhouette')
plt.show()
# Méthode du silhouette plot
# Instanciation du modèle kmeans
km = KMeans(n_clusters=k_rfm, n_init=10, init='k-means++', random_state=random_state)
# Instanciation du SilhouetteVisualizer instance
visualizer = SilhouetteVisualizer(km, colors='yellowbrick')
# entraînement du visualizer
visualizer.fit(X_rfm_scaled)
plt.show()
Nombre de clusters = 4¶
- Clustering
# Instanciation et Entraînement du modèle k-means
kmeans = KMeans(n_clusters=k_rfm, n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_rfm_scaled)
# Stockage des clusters dans la variable labels
labels_rfm_k4 = kmeans.labels_
# Stockage des centroids dans une variable
centroids_kmeans_rfm_k4 = kmeans.cluster_centers_
# Nombre de clients par cluster
unique, counts = np.unique(labels_rfm_k4+1, return_counts=True)
dict(zip(unique, counts))
{1: 37478, 2: 50390, 3: 2760, 4: 2414}
- Stabilité des clusters
Les tests montrent que les clusters sont stables.
# Tests de stabilité des clusters
for i in range(3):
kmeans_test = KMeans(n_clusters=k_rfm, n_init=10, init='k-means++')
kmeans_test.fit(X_rfm_scaled)
labels_test = kmeans_test.labels_
unique, counts = np.unique(labels_test + 1, return_counts=True)
print("Test", i + 1)
print(dict(zip(unique, counts)))
Test 1
{1: 37478, 2: 50390, 3: 2414, 4: 2760}
Test 2
{1: 37478, 2: 50390, 3: 2760, 4: 2414}
Test 3
{1: 50394, 2: 37475, 3: 2760, 4: 2413}
- Analyse des clusters
# Représentation des centroïdes sous forme de heatmap
centroids_kmeans_rfm_k4 = pd.DataFrame(centroids_kmeans_rfm_k4,
index = np.unique(labels_rfm_k4+1),
columns = features_rfm )
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroids_kmeans_rfm_k4.T, vmin=-2, vmax=2, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
# Ajouter 'Cluster_kmeans' au df et changer le type en string pour un affichage du graphique en couleur discrète
rfm_k4_df = rfm_df.copy()
rfm_k4_df.loc[:,'Cluster_kmeans'] = labels_rfm_k4+1
rfm_k4_df.loc[:,'Cluster_kmeans'] = rfm_k4_df['Cluster_kmeans'].astype(str)
# Ajouter l'index comme variable
centroids_kmeans_rfm_k4 = centroids_kmeans_rfm_k4.reset_index()
centroids_kmeans_rfm_k4 = centroids_kmeans_rfm_k4.rename(columns={'index':'Cluster_kmeans'})
# Critères d'identification des clusters
min_recency = min(centroids_kmeans_rfm_k4.iloc[:,1])
max_recency = max(centroids_kmeans_rfm_k4.iloc[:,1])
max_frequency = max(centroids_kmeans_rfm_k4.iloc[:,2])
max_monetary = max(centroids_kmeans_rfm_k4.iloc[:,3])
# Initialisation de la colonne 'segment'
centroids_kmeans_rfm_k4['segment'] = 'Other'
# Attribution des clusters
for row in range(0, len(centroids_kmeans_rfm_k4.index)):
if centroids_kmeans_rfm_k4['recency'][row] == min_recency:
centroids_kmeans_rfm_k4.loc[row,'segment'] = 'New customer'
elif centroids_kmeans_rfm_k4['recency'][row] == max_recency:
centroids_kmeans_rfm_k4.loc[row,'segment'] = 'Hibernating'
elif centroids_kmeans_rfm_k4['frequency'][row] == max_frequency:
centroids_kmeans_rfm_k4.loc[row,'segment'] = 'Champions'
elif centroids_kmeans_rfm_k4['monetary'][row] == max_monetary:
centroids_kmeans_rfm_k4.loc[row,'segment'] = "Can't lose"
centroids_kmeans_rfm_k4['Cluster_kmeans'] = centroids_kmeans_rfm_k4['Cluster_kmeans'].astype(str)
rfm_k4_df = pd.merge(rfm_k4_df, centroids_kmeans_rfm_k4[['Cluster_kmeans', 'segment']], on='Cluster_kmeans')
# Représentation des centroïdes sur des radar charts
plt.figure(figsize=(7, 7))
# Boucle pour afficher les radars charts
for row in range(0, len(centroids_kmeans_rfm_k4.index)):
make_spider(data=centroids_kmeans_rfm_k4.sort_values('Cluster_kmeans', ascending=True).iloc[:,1:],
group='segment',
row=row,
title='Cluster '+str(centroids_kmeans_rfm_k4['segment'][row]),
color=my_palette[row])
# Répartition des clients par segment
segment_counts = rfm_k4_df.groupby(['segment','Cluster_kmeans'], as_index=False).agg(count = ('recency','count')).sort_values('Cluster_kmeans', ascending=True)
segments = list(segment_counts['segment'].unique())
colors = my_palette[:len(segments)]
plt.pie(x=segment_counts['count'],
labels=segments,
autopct='%.1f%%',
pctdistance=0.8,
colors=colors)
plt.title("Répartition des clients par segment")
plt.tight_layout()
plt.show()
# Bloxplot par cluster et par variable
fig, ax = plt.subplots(1, 3, figsize=(20,5), tight_layout=True)
for i, col in enumerate(rfm_k4_df.iloc[:,:3].columns):
sns.boxplot(data=rfm_k4_df.sort_values('Cluster_kmeans', ascending=True),
x='segment',
y=col,
ax=ax[i],
hue='segment',
showmeans=True,
showfliers=False,
meanprops=meanprops,
palette = my_palette[:4])
ax[i].set_title(f'Distribution des données des clusters pour la variable {col}', wrap=True)
plt.show()
# Projection des clusters
fig = px.scatter_3d(
rfm_k4_df.sort_values('Cluster_kmeans', ascending=True), x='recency', y='frequency', z='monetary', color='segment',
title='Clustering K-means - RFM'
)
fig.show()
# Transformation du dataframe pour avoir les valeurs en colonne -> nécessaire pour la représentation en radar chart
centroids_kmeans_rfm_k4_melt = centroids_kmeans_rfm_k4.reset_index().melt(id_vars = ['segment', 'Cluster_kmeans'],
value_vars = features_rfm)
centroids_kmeans_rfm_k4_melt = centroids_kmeans_rfm_k4_melt.sort_values(['Cluster_kmeans', 'variable'])\
.drop(columns='Cluster_kmeans')
# Créer des traces pour chaque segment
traces = []
for segment in centroids_kmeans_rfm_k4_melt['segment'].unique():
segment_data = centroids_kmeans_rfm_k4_melt[centroids_kmeans_rfm_k4_melt['segment'] == segment]
# Ajouter le premier point à la fin pour fermer la ligne
r_values = segment_data['value'].tolist() + [segment_data['value'].iloc[0]]
theta_values = segment_data['variable'].tolist() + [segment_data['variable'].iloc[0]]
trace = go.Scatterpolar(
r=r_values,
theta=theta_values,
mode='lines',
name=str(segment),
fill='toself' # Remplir l'aire sous la courbe
)
traces.append(trace)
# Créer la mise en page
layout = go.Layout(
title="Comparaison des centroïdes des Cluster_kmeans RFM"
)
# Créer la figure
fig = go.Figure(data=traces, layout=layout)
# Afficher la figure
fig.show()
Nombre de clusters = 6¶
Testons maintenant avec k=6 clusters si nous retrouvons les clusters identifiés avec la RFM marketing.
- Clustering
# Définition du nombre de clusters
k = 6
# Instanciation et Entraînement du modèle k-means
kmeans = KMeans(n_clusters=k,n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_rfm_scaled)
# Stockage des clusters dans la variable labels
labels_rfm_k6 = kmeans.labels_
# Stockage des centroids dans une variable
centroids_kmeans_rfm_k6 = kmeans.cluster_centers_
# Nombre de clients par cluster
unique, counts = np.unique(labels_rfm_k6+1, return_counts=True)
dict(zip(unique, counts))
{1: 32327, 2: 2766, 3: 33016, 4: 504, 5: 20367, 6: 4062}
- Stabilité des clusters
Les tests montrent que les clusters sont stables.
# Tests de stabilité des clusters
for i in range(3):
kmeans_test = KMeans(n_clusters=k, n_init=10, init='k-means++')
kmeans_test.fit(X_rfm_scaled)
labels_test = kmeans_test.labels_
unique, counts = np.unique(labels_test + 1, return_counts=True)
print("Test", i + 1)
print(dict(zip(unique, counts)))
Test 1
{1: 32454, 2: 2767, 3: 32993, 4: 461, 5: 20454, 6: 3913}
Test 2
{1: 20417, 2: 32971, 3: 2767, 4: 499, 5: 32386, 6: 4002}
Test 3
{1: 32903, 2: 20488, 3: 2767, 4: 462, 5: 3918, 6: 32504}
- Analyse des clusters
# Représentation des centroïdes sous forme de heatmap
centroids_kmeans_rfm_k6 = pd.DataFrame(centroids_kmeans_rfm_k6,
index = np.unique(labels_rfm_k6+1),
columns = features_rfm )
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroids_kmeans_rfm_k6.T, vmin=-2, vmax=2, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
# Ajouter 'Cluster_kmeans' au df et changer le type en string pour un affichage du graphique en couleur discrète
rfm_k6_df = rfm_df.copy()
rfm_k6_df.loc[:,'Cluster_kmeans'] = labels_rfm_k6+1
rfm_k6_df.loc[:,'Cluster_kmeans'] = rfm_k6_df['Cluster_kmeans'].astype(str)
# Représentation des centroïdes sur des radar charts
centroids_kmeans_rfm_k6 = centroids_kmeans_rfm_k6.reset_index()
centroids_kmeans_rfm_k6 = centroids_kmeans_rfm_k6.rename(columns={'index':'Cluster_kmeans'})
# Critères d'identification des clusters
min_recency = min(centroids_kmeans_rfm_k6.iloc[:,1])
max_recency = max(centroids_kmeans_rfm_k6.iloc[:,1])
max_frequency = max(centroids_kmeans_rfm_k6.iloc[:,2])
min_monetary = min(centroids_kmeans_rfm_k6.iloc[:,3])
max_monetary = max(centroids_kmeans_rfm_k6.iloc[:,3])
# Initialisation de la colonne 'segment'
centroids_kmeans_rfm_k6['segment'] = 'Other'
# Attribution des clusters
for row in range(0, len(centroids_kmeans_rfm_k6.index)):
if centroids_kmeans_rfm_k6['recency'][row] == min_recency:
centroids_kmeans_rfm_k6.loc[row,'segment'] = 'New customers'
elif centroids_kmeans_rfm_k6['recency'][row] == max_recency:
centroids_kmeans_rfm_k6.loc[row,'segment'] = 'Hibernating'
elif centroids_kmeans_rfm_k6['frequency'][row] == max_frequency:
centroids_kmeans_rfm_k6.loc[row,'segment'] = 'Champions'
elif centroids_kmeans_rfm_k6['monetary'][row] == max_monetary:
centroids_kmeans_rfm_k6.loc[row,'segment'] = "Can't lose"
elif min_monetary < centroids_kmeans_rfm_k6['monetary'][row] < max_monetary:
centroids_kmeans_rfm_k6.loc[row,'segment'] = 'Need attention'
elif min_recency < centroids_kmeans_rfm_k6['recency'][row] < max_recency:
centroids_kmeans_rfm_k6.loc[row,'segment'] = 'Potential loyalists'
centroids_kmeans_rfm_k6['Cluster_kmeans'] = centroids_kmeans_rfm_k6['Cluster_kmeans'].astype(str)
rfm_k6_df = pd.merge(rfm_k6_df, centroids_kmeans_rfm_k6[['Cluster_kmeans', 'segment']], on='Cluster_kmeans')
# Représentation des centroïdes sur des radar charts
plt.figure(figsize=(10, 10))
# Boucle pour afficher les radars charts
for row in range(0, len(centroids_kmeans_rfm_k6.index)):
make_spider(data=centroids_kmeans_rfm_k6.sort_values('Cluster_kmeans', ascending=True).iloc[:,1:],
group='segment',
row=row,
title='Cluster '+str(centroids_kmeans_rfm_k6['segment'][row]),
color=my_palette[row])
# Répartition des clients par segment
segment_counts = rfm_k6_df.groupby(['segment','Cluster_kmeans'], as_index=False).agg(count = ('recency','count')).sort_values('Cluster_kmeans', ascending=True)
segments = list(segment_counts['segment'].unique())
colors = my_palette[:len(segments)]
plt.pie(x=segment_counts['count'],
labels=segments,
autopct='%.1f%%',
pctdistance=0.8,
colors=colors)
plt.title("Répartition des clients par segment")
plt.tight_layout()
plt.show()
# Bloxplot par cluster et par variable
fig, ax = plt.subplots(1, 3, figsize=(20,5), tight_layout=True)
for i, col in enumerate(rfm_k6_df.iloc[:,:3].columns):
sns.boxplot(data=rfm_k6_df.sort_values('Cluster_kmeans', ascending=True),
x='segment',
y=col,
ax=ax[i],
hue='segment',
showmeans=True,
showfliers=False,
meanprops=meanprops,
palette = my_palette[:6])
ax[i].set_title(f'Distribution des données des clusters pour la variable {col}', wrap=True)
ax[i].set_xticks(ax[i].get_xticks())
ax[i].set_xticklabels(ax[i].get_xticklabels(), rotation=45)
plt.show()
# Projection des clusters
fig = px.scatter_3d(
rfm_k6_df.sort_values('Cluster_kmeans', ascending=True), x='recency', y='frequency', z='monetary', color='segment',
title='Clustering K-means - RFM'
)
fig.show()
# Transformation du dataframe pour avoir les valeurs en colonne -> nécessaire pour la représentation en radar chart
centroids_kmeans_rfm_k6_melt = centroids_kmeans_rfm_k6.reset_index().melt(id_vars = ['segment', 'Cluster_kmeans'],
value_vars = features_rfm)
centroids_kmeans_rfm_k6_melt = centroids_kmeans_rfm_k6_melt.sort_values(['Cluster_kmeans', 'variable'])\
.drop(columns='Cluster_kmeans')
# Créer des traces pour chaque segment
traces = []
for segment in centroids_kmeans_rfm_k6_melt['segment'].unique():
segment_data = centroids_kmeans_rfm_k6_melt[centroids_kmeans_rfm_k6_melt['segment'] == segment]
# Ajouter le premier point à la fin pour fermer la ligne
r_values = segment_data['value'].tolist() + [segment_data['value'].iloc[0]]
theta_values = segment_data['variable'].tolist() + [segment_data['variable'].iloc[0]]
trace = go.Scatterpolar(
r=r_values,
theta=theta_values,
mode='lines',
name=str(segment),
fill='toself' # Remplir l'aire sous la courbe
)
traces.append(trace)
# Créer la mise en page
layout = go.Layout(
title="Comparaison des centroïdes des Cluster_kmeans RFM"
)
# Créer la figure
fig = go.Figure(data=traces, layout=layout)
# Afficher la figure
fig.show()
Classification Ascendante Hierarchique¶
Le jeu de données est trop important pour l'algorithme de CAH. Cette méthode n'est pas concluante.
Nous pouvons cependant tester la méthode sur un échantillon de notre jeu de données.
Méthode 1 - Echantillonage sur le jeu de données¶
# Échantillonnage aléatoire de 10% des données
sampled_data = rfm_df[['recency','frequency','monetary']].sample(frac=0.1, random_state=0)
display(Markdown(f"L'échantillon contient {sampled_data.shape[0]} clients."))
# Standardisation des données
X_sampled = sampled_data.values # Matrice des données
names_sample = sampled_data.index # Noms des individus
features = ['recency','frequency','monetary'] # Noms des variables
scaler_sample = StandardScaler() # Instanciation du scaler
X_sampled_scaled = scaler_sample.fit_transform(X_sampled) # Données scalées
L'échantillon contient 9304 clients.
- Dendogramme
# Linkage avec la méthode de Ward
Z = linkage(X_sampled_scaled, method="ward")
# Affichage du dendrogramme
fig, ax = plt.subplots(1, 1, figsize=(30,7))
dendogram_top = dendrogram(Z, ax=ax, orientation='top')
plt.title("Hierarchical Clustering Dendrogram", fontsize=15)
ax.set_ylabel("Distance")
ax.set_xlabel("Client")
ax.tick_params(axis='y', which='major', labelsize=15)
# définition du nombre de clusters k
k = 4
# Affichage du dendrogramme limité à k clusters
fig, ax = plt.subplots(1, 1, figsize=(7, 5))
dendogram_truncate = dendrogram(Z, p=k, truncate_mode="lastp", ax=ax) # découpage pour n'afficher que k clusters
plt.title("Hierarchical Clustering Dendrogram")
plt.xlabel("Number of points in node (or index of point if no parenthesis).")
plt.ylabel("Distance.")
plt.show()
# Définition des clusters
clusters = fcluster(Z, k, criterion='maxclust')
- Visualisation des clusters
# Coordonnées des centroïdes
centroid_CAH = pd.DataFrame(data=X_sampled_scaled, columns=['recency', 'frequency','monetary'])
centroid_CAH['Cluster_CAH'] = clusters
centroid_CAH = centroid_CAH.groupby('Cluster_CAH').mean()
# Représentation des centroïdes sous forme de heatmap
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroid_CAH.T, vmin=-2, vmax=2, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
# Ajouter 'Cluster_CAH à sampled_data et changer le type en string pour un affichage du graphique en couleur discrète
sampled_data['Cluster_CAH'] = clusters
sampled_data['Cluster_CAH'] = sampled_data['Cluster_CAH'].astype(str)
# Projection sur les 3 variables RFM - graphique 3D
fig = px.scatter_3d(
sampled_data.sort_values('Cluster_CAH', ascending=True), x='recency', y='frequency', z='monetary', color='Cluster_CAH',
title='Clustering CAH'
)
fig.show()
# Représentation des centroïdes sur des radar charts
data = centroid_CAH.reset_index()
plt.figure(figsize=(7, 7))
# Boucle pour afficher les radars charts
for row in range(0, len(data.index)):
make_spider(data=data,
group='Cluster_CAH',
row=row,
title='Cluster '+str(data['Cluster_CAH'][row]),
color=my_palette[row])
Méthode 2 - Clustering k-means puis CAH¶
- Clustering K-means k=1000
k=1000
X = X_rfm_scaled
# Instanciation et Entraînement du modèle k-means
kmeans = KMeans(n_clusters=k,n_init=10, init='k-means++')
kmeans.fit(X)
# Stockage des clusters dans la variable labels
labels = kmeans.labels_
# Stockage des centroids dans une variable
centroids_kmeans_rfm = kmeans.cluster_centers_
- Dendogramme
# Linkage avec la méthode de Ward
Z = linkage(centroids_kmeans_rfm, method="ward")
# Affichage du dendrogramme
fig, ax = plt.subplots(1, 1, figsize=(30,7))
dendogram_top = dendrogram(Z, ax=ax, orientation='top')
plt.title("Hierarchical Clustering Dendrogram", fontsize=15)
ax.set_ylabel("Distance")
ax.set_xlabel("Groupe de Clients")
ax.tick_params(axis='y', which='major', labelsize=15)
# définition du nombre de clusters k
k = 4
# Affichage du dendrogramme limité à k clusters
fig, ax = plt.subplots(1, 1, figsize=(7, 5))
dendogram_truncate = dendrogram(Z, p=k, truncate_mode="lastp", ax=ax) # découpage pour n'afficher que k clusters
plt.title("Hierarchical Clustering Dendrogram")
plt.xlabel("Number of points in node (or index of point if no parenthesis).")
plt.ylabel("Distance.")
plt.show()
# Définition des clusters
clusters = fcluster(Z, k, criterion='maxclust')
- Visualisation des clusters
# Coordonnées des centroïdes
centroid_CAH = pd.DataFrame(data=centroids_kmeans_rfm, columns=['recency', 'frequency','monetary'])
centroid_CAH['Cluster_CAH'] = clusters
centroid_CAH = centroid_CAH.groupby('Cluster_CAH').mean()
# Représentation des centroïdes sous forme de heatmap
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroid_CAH.T, vmin=-5, vmax=5, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
DBScan¶
Density-Based Spatial Clustering of Applications with Noise
L'algorithme DBSCAN est difficile à utiliser en très grande dimension et en effet, il ne fonctionne pas ici.
Clustering k-means puis DBScan sur les clusters¶
Les résultats du k-means k=1000 réalisés précédemment sont utilisés ici
- Déterminer le paramètre epsilon optimum avec la méthode du coude
# Calculer les distances des plus proches voisins
neighb = NearestNeighbors(n_neighbors=2*centroids_kmeans_rfm.shape[1]) # Créer un objet NearestNeighbors
nbrs=neighb.fit(centroids_kmeans_rfm) # Entraînement du modèle
distances,indices=nbrs.kneighbors(centroids_kmeans_rfm) # trouver les plus proches voisins
# Trier et afficher les résultats des distances
distances = np.sort(distances, axis = 0) # classer les distances
distances = distances[:, 1] # sélectionner la seconde colonne avec les distances
plt.plot(distances) # tracer les distances
plt.show()
# Zoom sur le coude
plt.xlim(990,1000)
plt.plot(distances)
best_k = 997
# only one line may be specified; full height
plt.axhline(y = distances[best_k], color = 'red', ls=':',label = 'axvline - full height')
print(f'best epsilon : {round(distances[best_k],2)}')
best_epsilon = round(distances[best_k],2)
best epsilon : 9.6
# Instanciation d'un objet DBSCAN avec les paramètres optimums
dbscan = DBSCAN(eps=best_epsilon, min_samples=2*centroids_kmeans_rfm.shape[1])
# Entraînement du modèle
dbscan.fit(centroids_kmeans_rfm)
DBSCAN(eps=9.6, min_samples=6)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DBSCAN(eps=9.6, min_samples=6)
# Stockage des clusters dans la variable labels
labels_dbscan = dbscan.labels_
# Number of clusters in labels, ignoring noise if present.
n_clusters_ = len(set(labels_dbscan)) - (1 if -1 in labels_dbscan else 0)
n_noise_ = list(labels_dbscan).count(-1)
print("Estimated number of clusters: %d" % n_clusters_)
print("Estimated number of noise points: %d" % n_noise_)
Estimated number of clusters: 1 Estimated number of noise points: 2
# Projection sur les 3 variables RFM - graphique 3D
fig = px.scatter_3d(
centroids_kmeans_rfm, x=0, y=1, z=2, color=labels_dbscan,
title='Clustering DBScan'
)
fig.show()
Le modèle DBScan n'est pas pertinent ici : la distribution de la fréquence non normale rend les données trop denses et l'algorithme ne peut pas détecter de clusters.
Partie 3 - Clustering avec les variables RFM et Satisfaction client¶
La méthode utilisée ici sera le k-means, méthode la plus adaptée d'après les essais de la partie 2. Les variables testées sont : RFM + satisfaction client
Standardisation des données¶
rfms_df = df_cleaned[['recency','frequency','monetary', 'review_score_class']]
# Centrage et réduction des variables
X_rfms = rfms_df.values # Matrice des données
names = rfms_df.index # Noms des individus
features_rfms = rfms_df.columns # Noms des variables
scaler = StandardScaler() # Instanciation du scaler
X_rfms_scaled = scaler.fit_transform(X_rfms) # Données scalées
Nombre optimal de clusters¶
# définitiondu random_state et k range
k_min = 2
k_max = 10
random_state = 42
# Distortion score with KElbow visualizer
# Instanciation du modèle de clustering et du visualizer
km = KMeans(n_init = 10, init='k-means++', random_state=random_state)
kvisualizer = KElbowVisualizer(km, k=(k_min, k_max))
# Entraînement du visualizer et affichage du graphique
kvisualizer.fit(X_rfms_scaled)
kvisualizer.show()
# Récupération de la valeur du coude
k_rfms = kvisualizer.elbow_value_
# Silhouette score
silhouette_scores = []
for k in range(k_min, k_max):
kmeans = KMeans(n_clusters=k, n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_rfms_scaled)
labels = kmeans.labels_
silhouette_scores.append(silhouette_score(X_rfm_scaled, labels))
# Représentation graphique du Silhouette scor
plt.plot(range(k_min, k_max), silhouette_scores)
plt.title('Coefficient de silhouette pour chaque nombre de clusters k')
plt.xlabel('Nombre de clusters')
plt.ylabel('Coefficient de silhouette')
plt.show()
# Méthode du silhouette plot
# Instanciation du modèle kmeans
km = KMeans(n_clusters=k_rfms, n_init=10, init='k-means++', random_state=random_state)
# Instanciation du SilhouetteVisualizer instance
visualizer = SilhouetteVisualizer(km, colors='yellowbrick')
# entraînement du visualizer
visualizer.fit(X_rfm_scaled)
plt.show()
Clustering¶
# Instanciation et Entraînement du modèle k-means
kmeans = KMeans(n_clusters=k_rfms,n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_rfms_scaled)
# Stockage des clusters dans la variable labels
labels_rfms = kmeans.labels_
# Stockage des centroids dans une variable
centroids_kmeans_rfms = kmeans.cluster_centers_
# Nombre de clients par cluster
unique, counts = np.unique(labels_rfms+1, return_counts=True)
dict(zip(unique, counts))
{1: 29915, 2: 18724, 3: 39641, 4: 2760, 5: 2002}
- Stabilité des clusters
Les tests montrent que les clusters sont stables.
# Tests de stabilité des clusters
for i in range(3):
kmeans_test = KMeans(n_clusters=k_rfms, n_init=10, init='k-means++')
kmeans_test.fit(X_rfms_scaled)
labels_test = kmeans_test.labels_
unique, counts = np.unique(labels_test + 1, return_counts=True)
print("Test", i + 1)
print(dict(zip(unique, counts)))
Test 1
{1: 29915, 2: 39641, 3: 18724, 4: 2760, 5: 2002}
Test 2
{1: 2760, 2: 39641, 3: 18724, 4: 29915, 5: 2002}
Test 3
{1: 39641, 2: 18724, 3: 2760, 4: 2002, 5: 29915}
Analyse des clusters¶
# Représentation des centroïdes sous forme de heatmap
centroids_kmeans_rfms = pd.DataFrame(centroids_kmeans_rfms,
index = np.unique(labels_rfms+1),
columns = features_rfms )
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroids_kmeans_rfms.T, vmin=-2, vmax=2, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
# Ajouter 'Cluster_kmeans au df et changer le type en string pour un affichage du graphique en couleur discrète
rfms_df = rfms_df.copy()
rfms_df.loc[:, 'Cluster_kmeans'] = labels_rfms + 1
rfms_df.loc[:, 'Cluster_kmeans'] = rfms_df['Cluster_kmeans'].astype(str)
# Ajout de l'index 'cluster_kmeans' en variable
centroids_kmeans_rfms = centroids_kmeans_rfms.reset_index()
centroids_kmeans_rfms = centroids_kmeans_rfms.rename(columns={'index':'Cluster_kmeans'})
# Identification des clusters
min_recency = min(centroids_kmeans_rfms.iloc[:,1])
max_recency = max(centroids_kmeans_rfms.iloc[:,1])
max_frequency = max(centroids_kmeans_rfms.iloc[:,2])
max_monetary = max(centroids_kmeans_rfms.iloc[:,3])
min_review_score_class = min(centroids_kmeans_rfms.iloc[:,4])
# Initialisation de la colonne 'segment'
centroids_kmeans_rfms['segment'] = 'Other'
for row in range(0, len(centroids_kmeans_rfms.index)):
if centroids_kmeans_rfms['recency'][row] == max_recency:
centroids_kmeans_rfms.loc[row,'segment'] = 'Hibernating'
elif centroids_kmeans_rfms['recency'][row] == min_recency:
centroids_kmeans_rfms.loc[row,'segment'] = 'New customer'
elif centroids_kmeans_rfms['frequency'][row] == max_frequency:
centroids_kmeans_rfms.loc[row,'segment'] = 'Loyalist'
elif centroids_kmeans_rfms['monetary'][row] == max_monetary:
centroids_kmeans_rfms.loc[row,'segment'] = "Can't lose"
elif centroids_kmeans_rfms['review_score_class'][row] == min_review_score_class:
centroids_kmeans_rfms.loc[row,'segment'] = "Dissatisfied"
centroids_kmeans_rfms['Cluster_kmeans'] = centroids_kmeans_rfms['Cluster_kmeans'].astype(str)
rfms_df = pd.merge(rfms_df, centroids_kmeans_rfms[['Cluster_kmeans', 'segment']], on='Cluster_kmeans')
# Représentation des centroïdes sur des radar charts
plt.figure(figsize=(15, 20))
# Boucle pour afficher les radars charts
for row in range(0, len(centroids_kmeans_rfms.index)):
make_spider(data=centroids_kmeans_rfms.iloc[:,1:],
group='segment',
row=row,
title='Cluster '+str(centroids_kmeans_rfms['segment'][row]),
color=my_palette[row])
# Répartition des clients par segment
segment_counts = rfms_df.groupby(['segment','Cluster_kmeans'], as_index=False).agg(count = ('recency','count')).sort_values('Cluster_kmeans', ascending=True)
segments = list(segment_counts['segment'].unique())
colors = my_palette[:len(segments)]
plt.pie(x=segment_counts['count'],
labels=segments,
autopct='%.1f%%',
pctdistance=0.8,
colors=colors)
plt.title("Répartition des clients par segment")
plt.tight_layout()
plt.show()
# Bloxplot par cluster et par variable
fig, ax = plt.subplots(1, 4, figsize=(25,5), tight_layout=True)
for i, col in enumerate(rfms_df.iloc[:,:4].columns):
sns.boxplot(data=rfms_df.sort_values('Cluster_kmeans'),
x='segment',
y=col,
ax=ax[i],
hue='segment',
showmeans=True,
showfliers=False,
meanprops=meanprops,
palette = my_palette[:k_rfms])
ax[i].set_title(f'Distribution des données des clusters pour la variable {col}', wrap=True)
plt.show()
# Projection des clusters
fig = px.scatter_3d(
rfms_df.sort_values('Cluster_kmeans', ascending=True), x='recency', y='frequency', z='monetary', color='segment',
title='Clustering K-means - RFM + satisfaction'
)
fig.show()
# Projection des clusters
fig = px.scatter_3d(
rfms_df.sort_values('Cluster_kmeans', ascending=True),
x='recency',
y='review_score_class',
z='monetary',
color='segment',
title='Clustering K-means - RFM + satisfaction'
)
fig.show()
# Transformation du dataframe pour avoir les valeurs en colonne -> nécessaire pour la représentation en radar chart
centroids_kmeans_rfms_melt = centroids_kmeans_rfms.reset_index().melt(id_vars = ['segment', 'Cluster_kmeans'],
value_vars = features_rfms)
centroids_kmeans_rfms_melt = centroids_kmeans_rfms_melt.sort_values(['Cluster_kmeans', 'variable'])\
.drop(columns='Cluster_kmeans')
# Créer des traces pour chaque segment
traces = []
for segment in centroids_kmeans_rfms_melt['segment'].unique():
segment_data = centroids_kmeans_rfms_melt[centroids_kmeans_rfms_melt['segment'] == segment]
# Ajouter le premier point à la fin pour fermer la ligne
r_values = segment_data['value'].tolist() + [segment_data['value'].iloc[0]]
theta_values = segment_data['variable'].tolist() + [segment_data['variable'].iloc[0]]
trace = go.Scatterpolar(
r=r_values,
theta=theta_values,
mode='lines',
name=str(segment),
fill='toself' # Remplir l'aire sous la courbe
)
traces.append(trace)
# Créer la mise en page
layout = go.Layout(
title="Comparaison des centroïdes des Cluster_kmeans RFMS"
)
# Créer la figure
fig = go.Figure(data=traces, layout=layout)
# Afficher la figure
fig.show()
Partie 4 - Clustering avec plus de variables¶
- Essai avec l'ensemble des variables (items / catégorie) : trop de features en entrée de l'ACP (>4000 après encodage)
- Essai avec 'geolocation_lat' + 'geolocation_lng' : non pertinent et ne permet pas de une bonne projection des individus. Une information du niveau de revenu par quartier serait plus pertinente.
- Essai avec 'customer_state' + encodage => non pertinent
Réduction de dimensions - PCA¶
- Transformation des variables
df_acp = df_cleaned[[ 'recency',
'frequency',
'monetary',
'average_basket_amount',
'review_score_class',
'mean_delivery_delay',
'average_basket',
'total_items',
'geolocation_lat',
'geolocation_lng'
]]
X = df_acp.values # Matrice des données
names = df_acp.index # Noms des individus
features = df_acp.columns # Noms des variables
p = df_acp.shape[1] # nb de variables
# Instanciation
scaler = StandardScaler()
# Fit and transform
X_scaled = scaler.fit_transform(X)
df_acp.shape
(93042, 10)
- Analyse en Composantes Principales
# Instanciation de l'ACP
pca = PCA()
# Entraînement sur les données scalées
pca.fit(X_scaled)
PCA()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
PCA()
scree = pd.DataFrame(
{
"Dimension" : ["Dim" + str(x + 1) for x in range(p)],
"Variance expliquée" : pca.explained_variance_,
"% variance expliquée" : np.round(pca.explained_variance_ratio_ * 100),
"% cum. var. expliquée" : np.round(np.cumsum(pca.explained_variance_ratio_) * 100)
}
)
scree
| Dimension | Variance expliquée | % variance expliquée | % cum. var. expliquée | |
|---|---|---|---|---|
| 0 | Dim1 | 2.300320 | 23.0 | 23.0 |
| 1 | Dim2 | 1.603475 | 16.0 | 39.0 |
| 2 | Dim3 | 1.431477 | 14.0 | 53.0 |
| 3 | Dim4 | 1.224288 | 12.0 | 66.0 |
| 4 | Dim5 | 1.021134 | 10.0 | 76.0 |
| 5 | Dim6 | 0.979954 | 10.0 | 86.0 |
| 6 | Dim7 | 0.737780 | 7.0 | 93.0 |
| 7 | Dim8 | 0.544252 | 5.0 | 98.0 |
| 8 | Dim9 | 0.150137 | 2.0 | 100.0 |
| 9 | Dim10 | 0.007290 | 0.0 | 100.0 |
# liste des composants (indice des composantes)
x_list = range(1, p+1)
list(x_list)
# Représentation graphiques des valeurs propres
plt.bar(x_list, scree['% variance expliquée'])
plt.plot(x_list, scree['% cum. var. expliquée'],c="red",marker='o')
plt.xlabel("rang de l'axe d'inertie")
plt.ylabel("pourcentage d'inertie")
plt.title("Eboulis des valeurs propres")
plt.show(block=False)
# Représentation graphiques des valeurs propres - méthode du coude
plt.plot(x_list, scree['% variance expliquée'],marker='o')
plt.xlabel("rang de l'axe d'inertie")
plt.ylabel("pourcentage d'inertie")
plt.title("Eboulis des valeurs propres")
plt.show(block=False)
# Nombre de composantes principales
n_components = 5
# Instanciation de l'ACP
pca = PCA(n_components=n_components)
# Entraînement sur les données scalées
pca.fit(X_scaled)
PCA(n_components=5)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
PCA(n_components=5)
# Principal Components = PCs
pcs = pd.DataFrame(pca.components_)
# affichage de 'features' pour les colonnes et Fi en index
pcs.columns = features
# liste des composants (indice des composantes)
x_list = range(1, n_components+1)
list(x_list)
pcs.index = [f"PC{i}" for i in x_list]
pcs.round(2)
# Représentation sous forme de heatmap des 5 premières composantes
fig, ax = plt.subplots(figsize=(20, 10))
sns.heatmap(pcs.T.iloc[:,:n_components], vmin=-1, vmax=1, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
- PC1 : plus le client achète d'articles, plus il dépense
- PC2 : clients qui achètent beaucoup d'articles à petits prix
- PC3 : localisation geographique
- PC4 : plus le délai de livraison est rapide, plus le client est satisfait
- PC5 : plus le dernier achat est récent, plus la fréquence est élevée
Nous retrouvons ici la corrélation entre 'monetary' et 'average_basket_amount', 'total_items' et 'average_basket'.
# Graphe de corrélation pour PC1 et PC2
correlation_graph(pca, (0,1), features)
# Graphe de corrélation pour PC3 et PC4
correlation_graph(pca, (2,3), features)
- Projection des individus
X_proj = pca.transform(X_scaled)
display_factorial_planes(X_proj,
[0,1],
pca,
figsize=(20,16),
clusters=df_acp['review_score_class'],
marker="o"
)
Clustering k-means¶
Nombre optimum de clusters k¶
# définition de k range
k_min = 2
k_max = 10
random_state = 42
# Distortion score with KElbow visualizer
# Instanciation du modèle de clustering et du visualizer
km = KMeans(n_init=10, init='k-means++', random_state = random_state)
visualizer = KElbowVisualizer(km, k=(k_min, k_max))
# Entraînement du visualizer et affichage du graphique
visualizer.fit(X_proj)
visualizer.show()
plt.show()
# Récupération de la valeur du coude
k_acp = visualizer.elbow_value_
# Silhouette score
silhouette_scores = []
for k in range(k_min, k_max):
kmeans = KMeans(n_clusters=k, n_init=10, init='k-means++', random_state = random_state)
kmeans.fit(X_proj)
labels = kmeans.labels_
silhouette_scores.append(silhouette_score(X_rfm_scaled, labels))
# Représentation graphique du Silhouette scor
plt.plot(range(k_min, k_max), silhouette_scores)
plt.title('Coefficient de silhouette pour chaque nombre de clusters k')
plt.xlabel('Nombre de clusters')
plt.ylabel('Coefficient de silhouette')
plt.show()
Clustering¶
# Instanciation et Entraînement du modèle k-means
kmeans = KMeans(n_clusters=k_acp,n_init=10, init='k-means++', random_state=random_state)
kmeans.fit(X_proj)
# Stockage des clusters dans la variable labels
labels = kmeans.labels_
# Stockage des centroids dans une variable
centroids_kmeans_acp = kmeans.cluster_centers_
# Nombre de clients par cluster
unique, counts = np.unique(labels+1, return_counts=True)
dict(zip(unique, counts))
{1: 2242, 2: 9455, 3: 64112, 4: 16325, 5: 908}
- Stabilité des clusters
Les tests montrent que les clusters sont stables.
# Tests de stabilité des clusters
for i in range(3):
kmeans_test = KMeans(n_clusters=k_acp, n_init=10, init='k-means++')
kmeans_test.fit(X_proj)
labels_test = kmeans_test.labels_
unique, counts = np.unique(labels_test + 1, return_counts=True)
print("Test", i + 1)
print(dict(zip(unique, counts)))
Test 1
{1: 16332, 2: 64143, 3: 2199, 4: 908, 5: 9460}
Test 2
{1: 64112, 2: 16325, 3: 908, 4: 2242, 5: 9455}
Test 3
{1: 64117, 2: 16321, 3: 908, 4: 9454, 5: 2242}
Réprésentation des clusters¶
# Affichage des clusters sur le premier plan factoriel de l'ACP
fig, ax = plt.subplots(1,1, figsize=(10,10))
sns.scatterplot(data=None,
x=X_proj[:, 0],
y=X_proj[:, 1],
hue=labels+1,
palette=my_palette[:k_acp],
ax=ax)# Affichage des individus
sns.scatterplot(data=None,
x=centroids_kmeans_acp[:, 0],
y=centroids_kmeans_acp[:, 1],
marker="s",
c="black",
ax=ax)# Affichage des centroïdes
ax.set_xlabel("PC1")
ax.set_ylabel("PC2")
ax.set_title("K-means : Projection des clusters et des centroïdes sur le premier plan factoriel")
plt.xlim(min(X_proj[:,0])-1,max(X_proj[:,0])+1)
plt.ylim(min(X_proj[:,1])-1,max(X_proj[:,1])+1)
plt.show()
# Affichage des clusters sur le premier plan factoriel de l'ACP
fig, ax = plt.subplots(1,1, figsize=(10,10))
sns.scatterplot(data=None,
x=X_proj[:, 2],
y=X_proj[:, 3],
hue=labels+1,
palette=my_palette[:k_acp],
ax=ax)# Affichage des individus
sns.scatterplot(data=None,
x=centroids_kmeans_acp[:, 2],
y=centroids_kmeans_acp[:, 3],
marker="s",
c="black",
ax=ax)# Affichage des centroïdes
ax.set_xlabel("PC1")
ax.set_ylabel("PC2")
ax.set_title("K-means : Projection des clusters et des centroïdes sur le deuxième plan factoriel")
plt.xlim(min(X_proj[:,2])-1,max(X_proj[:,2])+1)
plt.ylim(min(X_proj[:,3])-1,max(X_proj[:,3])+1)
plt.show()
Cluster 3 - Beaucoup d'articles Cluster 2 - Grosses dépenses
# Dataframe avec les clusters
df_kmeans = pd.DataFrame(data=X_proj,
columns=['PC1','PC2','PC3','PC4','PC5'],
index=names)
df_kmeans.loc[:,'cluster_kmeans'] = labels+1
df_kmeans = df_kmeans.sort_values('cluster_kmeans')
df_kmeans.loc[:,'cluster_kmeans'] = df_kmeans['cluster_kmeans'].astype(str)
# Projection des clusters sur les 3 premières composantes de l'ACP
fig = px.scatter_3d(
df_kmeans, x='PC1', y='PC2', z='PC3', color='cluster_kmeans',
title=f'Clustering K-means - Total Explained Variance: {scree.iloc[2,3]:.2f}%'
)
fig.show()
Analyse des clusters¶
# Représentation des centroïdes sous forme de heatmap
centroids_kmeans_acp_df = pd.DataFrame(centroids_kmeans_acp,
index = np.unique(labels+1),
columns = ['PC1','PC2','PC3','PC4','PC5'] )
fig, ax = plt.subplots(figsize=(10, 5))
sns.heatmap(centroids_kmeans_acp_df.T, vmin=-2, vmax=2, annot=True, cmap="coolwarm", fmt="0.2f")
plt.show()
# Ajout de la colonne cluster_kmeans
centroids_kmeans_acp_df = centroids_kmeans_acp_df.reset_index()
centroids_kmeans_acp_df = centroids_kmeans_acp_df.rename(columns={'index':'cluster_kmeans'})
# Représentation des centroïdes sur des radar charts
plt.figure(figsize=(10, 15))
# Boucle pour afficher les radars charts
for row in range(0, len(centroids_kmeans_acp_df.index)):
make_spider(data=centroids_kmeans_acp_df,
group='cluster_kmeans',
row=row,
title='Cluster '+str(centroids_kmeans_acp_df['cluster_kmeans'][row]),
color=my_palette[row])
Conclusion & Perspectives¶
Conclusion
- RFM Marketing :
- avantages :
- simple à comprendre et à mettre en œuvre
- permet d'identifier les segments de clients les plus rentables, tels que les clients les plus fidèles ou ceux qui dépensent le plus, ce qui permet aux entreprises de concentrer leurs efforts marketing et leurs ressources là où elles auront le plus d'impact.
- inconvénients :
- ne prend en compte que trois dimensions du comportement d'achat des clients (récence, fréquence, montant), ce qui peut ne pas suffire pour capturer tous les aspects de la relation client
- biaisée par la définition des classes avec les quantiles
- avantages :
- Clustering avec RFM :
- Méthode retenue k-means : la taille du jeu de données ne permet pas d'utiliser CAH et DBScan
- Les 3 variables RFM permettent une bonne interprétation des segments.
- Clustering avec RFM + satisfaction :
- apporte plus d'informations sur les clients et permet encore d'avoir une définition actionnable des segments
- Plus de variables ACP + kmeans :
- Difficulté d'interprétation des clusters avec les composantes principales. Perte de pertinence et de sens métier tangible
Perspectives
- Réaliser une segmentation sur les évènements micros tels que les Black Friday pour analyser les profil des clients et cibler ceux qui pourraient revenir (achat de produits consommables par exemple).